Shiro CAS统一认证集成导致的JSESSIONID问题

版本信息

应用系统信息

  • Spring(4.3.18.RELEASE) + Spring MVC(4.3.18.RELEASE) + Mybatis(3.4.6)
  • Shiro(shiro-web 1.2.3 + shiro+spring 1.2.3 + shiro+cas 1.2.3)
  • 操作系统:Ubuntu14.04 LTS amd64
  • Tomcat 8.5.39
  • Nginx 1.4.6
  • JDK 1.8.0_181

统一认证平台系统信息

  • 由于集成的是学校的统一认证平台,尚不清楚是具体的信息,只提供了http的域名
  • 使用了 Shiro + CAS 实现了单点登录

背景

最近在做一个基于NB-IoT的物联网智能门锁项目,后台使用SSM实现的一个统一的智能门锁管理系统,一开始有一套自己的用户,密码机制。

需求

  • 应用系统需要集成统一认证平台,实现单点登录
  • 微信小程序实现单点登录统一认证平台,然后访问系统,获取信息

问题

  • 摒弃应用系统的原有的一套密码机制,实现单点登录(卧槽没有单点登录的经验啊,咋弄?)
  • 微信小程序统一认证授权,一般只有微信呀、微博等的授权机制(自建统一认证平台,咋实现统一认证登录?)
  • 微信小程序发布时需要有HTTPS的域名(学校提供的只有HTTP的,又不能增加HTTPS,咋弄?)

头大、头大、头大

过程

没办法,硬着头皮也得来呀,心里一万个难受。

应用系统集成统一认证平台

  1. 集成学校统一认证平台的提供了文档,按照上面的来配置,改了Web.xml,体检了一些监听器和过滤器,重启系统,跳转,卧槽可以了呀?登录。。。。。。失败。。。。。。没反应?
  2. 然后又是疯狂的看文档,跑学校提供的测试Demo,没错啊,就这样,就这样配的啊,开始自我怀疑了。。。。。。
  3. 查资料,各种Google、Baidu,突然发现Shiro+CAS实现单点登录? 文档中提到了也是CAS的???,这不是一样的吗?
  4. 然后按照网上提供的教程,重写shiro的配置文件,原来应用系统提供了ShiroDBrealm的认证,现在是ShiroCasrealm的认证方式。配完之后发现,没有统一认证服务器啊?咋办?学校那边在后台添加了我的这认证的那个域名,本机没法弄啊,神奇1的Github派上用场了,弄个测试服务器啊(^_^) spring-shiro-cas
  5. 放在tomcat里面,在本机上其他端口跑起来,应用系统配置写好对应的配置文件跑起来。登录成功。

下面是Shiro的配置文件

1
2
3
4
5
6
shiro.loginUrl=http://192.168.10.185:8081/cas/login?service=http://192.168.10.185:8080/cas
shiro.logoutUrl=http://192.168.10.185:8081/cas/logout?service=http://192.168.10.185:8080/cas/logout
shiro.cas.serverUrlPrefix=http://192.168.10.185:8081/cas
shiro.cas.service=http://192.168.10.185:8080/cas
shiro.successUrl=/ids
shiro.failureUrl=http://192.168.10.185:8081/cas/login?service=http://192.168.10.185:8080/failure.html

spring-shiro-cas.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
default-lazy-init="true">

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="${shiro.loginUrl}"/>
<property name="successUrl" value="${shiro.successUrl}"/>
<property name="filters">
<map>
<entry key="casFilter" value-ref="casFilter"/>
<entry key="logoutFilter" value-ref="logoutFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<value>
/cas = casFilter
/cas/logout = logoutFilter
/assets/** = anon
/VPiWcrOOd1.txt = anon
/**.html = anon
/** = authc
</value>
</property>
</bean>

<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
<property name="failureUrl" value="${shiro.failureUrl}"/>
</bean>

<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="${shiro.logoutUrl}"/>
</bean>
<bean id="casRealm" class="com.microthings.lock.security.shiro.ShiroCasRealm">
<property name="casServerUrlPrefix" value="${shiro.cas.serverUrlPrefix}"/>
<property name="casService" value="${shiro.cas.service}"/>
</bean>

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="casRealm"/>
<property name="subjectFactory" ref="casSubjectFactory"/>
</bean>

<bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory"/>

<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>

<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
<property name="arguments" ref="securityManager"/>
</bean>
</beans>

替换成学校的统一认证接口,部署发现可以了。

微信小程序集成统一认证平台

查阅文档,小程序好像没有集成第三方认证系统的能力啊?貌似自由微信、微博啥的统一授权的,自建的第三方系统咋集成啊

小程序使用的是DCloud公司的uni-app开发的,在微信官方文档中有一种方式可以打开网页,使用web-view,咦。。。。。。好像发现新大陆了。

参考教程:在web-view加载的本地及远程HTML中调用uni的API及网页和vue页面通讯

这种方式可以需要修改服务端,添加页面,同时也需要改动微信微信小程序端的逻辑,实现web-view网页与小程序内的应用的通信。可行!

下面是我在微信小程序端的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// login.vue 
<template>
<view>
<button @tap="ids">统一授权登录</button>
</view>
</template>

<script>
methods:{
ids: function(){
uni.navigateTo({
url: "./ids"
})
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ids.vue
<template>
<view>
<web-view src="https://xxx.cn" @message="handleMessage"></web-view>
</view>
</template>


<script>
methods:{
handleMessage(evt) {
//通过这种方式可以获取获取网页发过来的数据,是一个数组
var dataArray = evt.detail.data;
}
}
</script>

服务端的返回的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

<! DOCTYPE html>

<html lang="zh">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no">
<head>
<title>统一认证登录</title>
<script src="/assets/uni/jweixin-1.4.0.js"></script>
<script src="/assets/uni/uni.webview.0.1.52.js"></script>

<script>
document.addEventListener('UniAppJSBridgeReady', function (evt) {
uni.postMessage({
data: {
//写入发送的数据
}
});

// 跳转的方式,我这里是跳转的tabbar,看清楚是跳转到哪里,我当时写的没注意看发现跳转不回去,没反应,搞了好长时间
uni.switchTab({
url: '/pages/xxx/xxx/xxx'
});
});
</script>

</head>
<body>
<div class="main" style="padding-top: 110px;">
<h3>正在跳转......请稍候!</h3>
</div>
</body>
</html>

这样一套下来就能实现登录、跳转、返回了。感觉还不错,突然想到,使用web-view需要有业务域名啊?还需要是https的,咋办?

业务域名好办,使用公司信息注册一下,https呢?在和学校的信息处沟通后发现,他们没法改添加https,哇咔咔,开始停工了。

老板开始问,除了小程序还能使用h5吗?,我说可以,但是不能使用蓝牙开门。。。。。。h5+呢?h5+是做APP的,咱们不能小程序,只有小程序了呀~~~

经过一两天的折腾后,老板问了下别人,有个技术说可以使用nginx反向代理啊或者弄个二级域名?想想自己的域名就是学校分配的二级域名,哪还有别的二级域名?nginx反向代理,使用https代理http?嗯~好像可行,一语惊醒。就这么搞,第二天开始弄。

卸载原来的Apache,安装Nginx,添加反向代理,👌了,这么简单吗?使用我系统的Chrome(macOS)、IPhone登录一下,我艹这么简单?高兴的说解决了,然后对旁边的狗振说,你登一下(Windows Chrome)。。。BUG发生了,WTF登不上。再用我电脑的FireFox和Safari登录,没问题啊,又换用彭狗子的Chrome(Windows)、Android登录,登不上。😔彻底心碎了,然后开始各种查。问于狗子、金涛、学校的技术人员,都没遇见过。

然后开始分析问题

结构图

登录的处理过程是,首先浏览器通过https链接匹配访问到应用系统,发现该用户没有登录,shiro拦截请求,并重定向到配置的统一认证服务器上,要求输入用户名和密码,登录成功后携带ticket票据到应用系统,应用系统不知道该票据是否有效,然后又发送请求到认证服务器,请求验证票据,验证成功后,回到应用系统完成登录、授权等逻辑。

通过登录失败的例子发现,客户端请求登录跳转到统一认证服务器上、说明能够通过反向代理进入到统一认证服务器,知识post过去,服务器没能正确的处理响应,返回ticket,而是登录没成功又重定向到了登录界面。一开始以为是nginx的反向代理没处理好,于是改源码,前面加一个字符,发现可以登录,难道是服务端的问题??不存在啊,加完字符后发现,不行呀,这太麻烦了,所有的静态文件、请求的路径都需要加上前缀,然后放弃了。继续测,要不在nginx那端做一下判断,看到底是否进错了匹配路径?,于是加上了\$uri 和 \$request_method 判断,结构发现没有啊,说明POST请求确实到了统一认证服务器,但返回了不是预期的结果。此时已经是今天第五天了。

上午突然想想到底为啥有的浏览器成功,有的浏览器失败呢,于是分析HTTP请求。首先分析了FireFox的登录成功的请求。

初次访问系统的时候系统在cookie中放置了一个JSESSIONID

firefox_1

随后浏览器Get重定向的连接访问统一认证平台,同时设置了一个新的JSESSIONID,这个JESSIONID是由统一认证平台设置的,由统一认证平台系统识别。

firefox_2

在请求中发现在请求的style.css文件里面又重新Get请求/login,使用了上图幅👆的JESSIONID,服务端并且响应了。

firefox_3

随后在后面的请求中请求了favicon.ico图标,这个图标是统一平台的图标,因此系统又返回了一个JSESSIONID,这个是Nginx的配置问题,这个图标的根路径没有匹配进去,这导致了这个JESSIONID是应用系统返回的,于是you重定向到统一认证平台了。

firefox_4

然后填写用户名和密码,发送POST数据,发现登录失败,在请求中发现系统返回了一个新的JESSIONID,这个新的JESSIONID是被统一认证平台识别。

firefox_5

然后再次填写用户名和密码,发送POST请求,发现登录成功,系统返回了票据,随后就登陆成功了,同时再请求中请求的JESSIONID是上面👆的JESSIONID

firefox_6

通过上面的请求发现,这个JESSIONID是被两个系统来回切换的,在登录成功后发现应用系统又设置了新的JESSIONID。

分析完了FireFox的请求,我们来分析一下Chrome的登录请求。分析一下Windows的Chrome的登录请求。

第一次请求访问应用系统返回一个JESSIONID

chrome_1

然后重定向到认证服务请,请求返回一个JESSIONID
chrome_2

然后在style.css中有请求了/login,使用上面👆的JESSIONID

chrome_3

然后请求了favicon.ico文件,跳转到应用系统返回了应用系统的JESSIONID

chrome_4

再重定向到认证服务器,返回认真服务器新的JESSIONID

chrome_5

使用新的JESSIONID发送POST请求,理论上JESSIONID是认证服务器能够是别的为什么不可以呢?

chrome_6

其实这里有一个错误,但这个错误现在不知道为什么不能重现了,当时发现的错误是,Chrome里面的Cookie里面设置了两个JESSIONID,理论上说只可以存在一个key,为什么会有两个key呢?POST过去含有两个JESSIONID,服务端肯定会报错的,因此可以判定是由于两个JESSIONID导致了会话之间出现了问题,服务器无法正常识别,因此改动了一下自己应用系统的shiro的Cookie名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="casRealm"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="shiroCacheManager"/>
<property name="subjectFactory" ref="casSubjectFactory"/>
</bean>
<bean id="shiroSessionDao" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"/>

<bean id="shiroSimpleCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="SHAREJESSIONID"/>
<property name="maxAge" value="-1"/>
</bean>

<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="-1"/>
<property name="sessionDAO" ref="shiroSessionDao"/>
<property name="sessionIdCookie" ref="shiroSimpleCookie"/>
<property name="sessionValidationSchedulerEnabled" value="true"/>
</bean>

<bean id="shiroCacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>

更新(2019年10月25日 21:34)


在上面的分析中我们发现由于没有在nginx里面配置对统一认证服务器里面的/favicon.ico的匹配,导致当在请求该资源的时候,跳转到咱们的应用服务器,从而无法登陆,随后我在nginx上加了对favicon.ico的匹配规则,再吃使用未修改前的代码,使用不同操作系统的浏览器进行登录,发现均可以登录。唯一的不同之处是,用户在第一次访问应用服务器的时候,应用服务器返回的JESSIONID失效了,应为当认证服务器返回了JESSIONID就替换了原有的JESSIONID,当登录成功之后,应用服务器返回了新的JESSIONID,因此浪费了一次JESSIONID,从会话的角度讲,我觉着,这是一次完整的会话,只是涉及了两个不同应用程序不同域之间的交互,我上面的修改方式我觉着还是有必要的。

参考文章

  1. SpringMVC + Shiro 集成 CAS
  2. Cas单点登录及spring集成shiro-cas
  3. spring-shiro-cas
  4. 在web-view加载的本地及远程HTML中调用uni的API及网页和vue页面通讯

总结

通过这次这么长时间的尝试,修改,我也是收获很多,更重要的是要学会处理发生的问题,如何去分析,去解决,找到问题是关键。